/**
 * Copyright 2017 TerraMeta Software, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cloudgraph.rdb.service;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.sql.Blob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.plasma.sdo.DataFlavor;
import org.plasma.sdo.DataType;
import org.plasma.sdo.PlasmaProperty;
import org.plasma.sdo.helper.DataConverter;

import commonj.sdo.Property;
import commonj.sdo.Type;

public class RDBDataConverter {
  private static Log log = LogFactory.getFactory().getInstance(RDBDataConverter.class);

  static public volatile RDBDataConverter INSTANCE = initializeInstance();
  private Map<Integer, String> sqlTypeMap = new HashMap<Integer, String>();

  private RDBDataConverter() {

    // Get all field in java.sql.Types
    Field[] fields = java.sql.Types.class.getFields();
    for (int i = 0; i < fields.length; i++) {
      try {
        String name = fields[i].getName();
        Integer value = (Integer) fields[i].get(null);
        sqlTypeMap.put(value, name);
      } catch (IllegalAccessException e) {
      }
    }
  }

  private static synchronized RDBDataConverter initializeInstance() {
    if (INSTANCE == null)
      INSTANCE = new RDBDataConverter();
    return INSTANCE;
  }

  public Object fromJDBCDataType(ResultSet rs, int columnIndex, int sourceType,
      PlasmaProperty targetProperty) throws SQLException {

    Object result = convertFrom(rs, columnIndex, sourceType, targetProperty);
    return result;
  }

  public int toJDBCDataType(PlasmaProperty sourceProperty, Object value) throws SQLException {

    int result = convertToSqlType(sourceProperty, value);
    ;
    return result;
  }

  public Object toJDBCDataValue(PlasmaProperty sourceProperty, Object value) throws SQLException {

    Object result = convertToSqlValue(sourceProperty, value);

    return result;
  }

  public DataFlavor toJDBCDataFlavor(PlasmaProperty sourceProperty) {
    if (!sourceProperty.getType().isDataType())
      throw new IllegalArgumentException("expected data type property, not "
          + sourceProperty.toString());
    PlasmaProperty dataProperty = sourceProperty;
    return dataProperty.getDataFlavor();
  }

  private Object convertToSqlValue(Property property, Object value) throws SQLException {

    if (!property.getType().isDataType())
      throw new IllegalArgumentException("expected data type property, not " + property.toString());
    DataType dataType = DataType.valueOf(property.getType().getName());
    Object result;
    switch (dataType) {
    case String:
    case URI:
    case Month:
    case MonthDay:
    case Day:
    case Time:
    case Year:
    case YearMonth:
    case YearMonthDay:
    case Duration:
      result = DataConverter.INSTANCE.toString(property.getType(), value);
      break;
    case Date:
      // Plasma SDO allows more precision than just month/day/year
      // in an SDO date datatype, and using java.sql.Date will
      // truncate
      // here so use java.sql.Timestamp.
      Date date = DataConverter.INSTANCE.toDate(property.getType(), value);
      result = new java.sql.Timestamp(date.getTime());
      break;
    case DateTime:
      date = DataConverter.INSTANCE.toDate(property.getType(), value);
      result = new java.sql.Timestamp(date.getTime());
      break;
    case Decimal:
      result = DataConverter.INSTANCE.toDecimal(property.getType(), value);
      break;
    case Bytes:
      result = DataConverter.INSTANCE.toBytes(property.getType(), value);
      break;
    case Byte:
      result = DataConverter.INSTANCE.toByte(property.getType(), value);
      break;
    case Boolean:
      result = DataConverter.INSTANCE.toBoolean(property.getType(), value);
      break;
    case Character:
      result = DataConverter.INSTANCE.toString(property.getType(), value);
      break;
    case Double:
      result = DataConverter.INSTANCE.toDouble(property.getType(), value);
      break;
    case Float:
      result = DataConverter.INSTANCE.toDouble(property.getType(), value);
      break;
    case Int:
      result = DataConverter.INSTANCE.toInt(property.getType(), value);
      break;
    case Integer:
      result = DataConverter.INSTANCE.toInteger(property.getType(), value);
      break;
    case Long:
      result = DataConverter.INSTANCE.toLong(property.getType(), value);
      break;
    case Short:
      result = DataConverter.INSTANCE.toShort(property.getType(), value);
      break;
    case Strings:
      result = DataConverter.INSTANCE.toString(property.getType(), value);
      break;
    case Object:
    default:
      result = DataConverter.INSTANCE.toString(property.getType(), value);
      break;
    }

    return result;
  }

  private int convertToSqlType(Property property, Object value) {
    int result;
    if (!property.getType().isDataType())
      throw new IllegalArgumentException("expected data type property, not " + property.toString());
    DataType dataType = DataType.valueOf(property.getType().getName());
    switch (dataType) {
    case String:
    case URI:
    case Month:
    case MonthDay:
    case Day:
    case Time:
    case Year:
    case YearMonth:
    case YearMonthDay:
    case Duration:
    case Strings:
      result = java.sql.Types.VARCHAR;
      break;
    case Date:
      // Plasma SDO allows more precision than just month/day/year
      // in an SDO date datatype, and using java.sql.Date will
      // truncate
      // here so use java.sql.Timestamp.
      result = java.sql.Types.TIMESTAMP;
      break;
    case DateTime:
      result = java.sql.Types.TIMESTAMP;
      // FIXME: so what SDO datatype maps to a SQL timestamp??
      break;
    case Decimal:
      result = java.sql.Types.DECIMAL;
      break;
    case Bytes:
      // FIXME: how do we know whether a Blob here
      result = java.sql.Types.VARBINARY;
      break;
    case Byte:
      result = java.sql.Types.VARBINARY;
      break;
    case Boolean:
      result = java.sql.Types.BOOLEAN;
      break;
    case Character:
      result = java.sql.Types.CHAR;
      break;
    case Double:
      result = java.sql.Types.DOUBLE;
      break;
    case Float:
      result = java.sql.Types.FLOAT;
      break;
    case Int:
      result = java.sql.Types.INTEGER;
      break;
    case Integer:
      result = java.sql.Types.BIGINT;
      break;
    case Long:
      result = java.sql.Types.INTEGER; // FIXME: no JDBC long??
      break;
    case Short:
      result = java.sql.Types.SMALLINT;
      break;
    case Object:
    default:
      result = java.sql.Types.VARCHAR;
      break;
    }
    return result;
  }

  private Object convertFrom(ResultSet rs, int columnIndex, int sourceType, Property property)
      throws SQLException {
    Object result = null;
    if (!property.getType().isDataType())
      throw new IllegalArgumentException("expected data type property, not " + property.toString());
    DataType targetDataType = DataType.valueOf(property.getType().getName());
    switch (targetDataType) {
    case String:
    case URI:
    case Month:
    case MonthDay:
    case Day:
    case Time:
    case Year:
    case YearMonth:
    case YearMonthDay:
    case Duration:
      result = rs.getString(columnIndex);
      break;
    case Date:
      java.sql.Timestamp ts = rs.getTimestamp(columnIndex);
      if (ts != null)
        result = new java.util.Date(ts.getTime());
      break;
    case DateTime:
      ts = rs.getTimestamp(columnIndex);
      if (ts != null) {
        // format DateTime String for SDO
        java.util.Date date = new java.util.Date(ts.getTime());
        result = DataConverter.INSTANCE.getDateTimeFormat().format(date);
      }
      break;
    case Decimal:
      result = rs.getBigDecimal(columnIndex);
      break;
    case Bytes:
      if (sourceType != Types.BLOB) {
        result = rs.getBytes(columnIndex);
      } else if (sourceType == Types.BLOB) {
        Blob blob = rs.getBlob(columnIndex);
        if (blob != null) {
          long blobLen = blob.length(); // for debugging
          // Note: blob.getBytes(columnIndex, blob.length()); is
          // somehow truncating the array
          // by something like 14 bytes (?!!) even though
          // blob.length() returns the expected length
          // using getBinaryStream which is preferred anyway
          InputStream is = blob.getBinaryStream();
          try {
            byte[] bytes = IOUtils.toByteArray(is);
            long len = bytes.length; // for debugging
            result = bytes;
          } catch (IOException e) {
            throw new RDBServiceException(e);
          } finally {
            try {
              is.close();
            } catch (IOException e) {
              log.error(e.getMessage(), e);
            }
          }
        }
      }
      break;
    case Byte:
      result = rs.getByte(columnIndex);
      break;
    case Boolean:
      result = rs.getBoolean(columnIndex);
      break;
    case Character:
      result = rs.getInt(columnIndex);
      break;
    case Double:
      result = rs.getDouble(columnIndex);
      break;
    case Float:
      result = rs.getFloat(columnIndex);
      break;
    case Int:
      result = rs.getInt(columnIndex);
      break;
    case Integer:
      result = new BigInteger(rs.getString(columnIndex));
      break;
    case Long:
      result = rs.getLong(columnIndex);
      break;
    case Short:
      result = rs.getShort(columnIndex);
      break;
    case Strings:
      String value = rs.getString(columnIndex);
      if (value != null) {
        String[] values = value.split("\\s");
        List<String> list = new ArrayList<String>(values.length);
        for (int i = 0; i < values.length; i++)
          list.add(values[i]); // what no Java 5 sugar for this ??
        result = list;
      }
      break;
    case Object:
    default:
      result = rs.getObject(columnIndex);
      break;
    }
    return result;
  }

  public String toJDBCString(Type sourceType, PlasmaProperty sourceProperty, Object value) {

    String result = null;
    DataFlavor flavor = sourceProperty.getDataFlavor();

    switch (flavor) {
    case integral:
    case real:
      result = value.toString();
      break;
    case string:
      result = "'" + value.toString() + "'";
      break;
    default:
      result = value.toString();
    }

    return result;
  }

  public String getJdbcTypeName(int jdbcType) {

    return sqlTypeMap.get(jdbcType);
  }
}
